13.18 可视编程和Beans
迄今为止,我们已看到Java对创建可重复使用的代码片工作而言是多么的有价值。“最大限度地可重复使用”的代码单元拥有类,因为它包含一个紧密结合在一起的单元特性(字段)和单元动作(方法),它们可以直接经过混合或通过继承被重复使用。
继承和多态态性是面向对象编程的精华,但在大多数情况下当我们创建一个应用程序时,我们真正最想要的恰恰是我们最需要的组件。我们希望在我们的设计中设置这些部件就像电子工程师在电路板上创造集成电路块一样(在使用Java的情况下,就是放到WEB页面上)。这似乎会成为加快这种“模块集合”编制程序方法的发展。
“可视化编程”最早的成功——非常的成功——要归功于微软公司的Visual Basic(VB,可视化Basic语言),接下来的第二代是Borland公司Delphi(一种客户/服务器数据库应用程序开发工具,也是Java Beans设计的主要灵感)。这些编程工具的组件的像征就是可视化,这是不容置疑的,因为它们通常展示一些类型的可视化组件,例如:一个按惯或一个TextField。事实上,可视化通常表现为组件可以非常精确地访问运行中程序。因此可视化编程方法的一部分包含从一个调色盘从拖放一个组件并将它放置到我们的窗体中。应用程序创建工具像我们所做的一样编写程序代码,该代码将导致正在运行的程序中的组件被创建。
简单地拖放组件到一个窗体中通常不足以构成一个完整的程序。一般情况下,我们需要改变组件的特性,例如组件的色彩,组件的文字,组件连结的数据库,等等。特性可以参照属性在编程时进行修改。我们可以在应用程序构建工具中巧妙处置我们组件的属性,并且当我们创建程序时,构建数据被保存下来,所以当该程序被启动时,数据能被重新恢复。
到如今,我们可能习惯于使用对象的多个特性,这也是一个动作集合。在设计时,可视化组件的动作可由事件部分地代表,意味着“任何事件都可以发生在组件上”。通常,由我们决定想发生的事件,当一个事件发生时,对所发生的事件连接代码。
这是关键性的部分:应用程序构建工具可以动态地询问组件(利用映象)以发现组件支持的事件和属件。一旦它知道它们的状态,应用程序构建工具就可以显示组件的属性并允许我们修改它们的属性(当我们构建程序时,保存它们的状态),并且也显示这些事件。一般而言,我们做一些事件像双击一个事件以及应用程序构建工具创建一个代码并连接到事件上。当事件发生时,我们不得不编写执行代码。应用程序构建工具累计为我们做了大量的工作。结果我们可以注意到程序看起来像它所假定的那样运行,并且依赖应用程序构建工具去为我们管理连接的详细资料。可视化的编程工具如此成功的原因是它们明显加快构建的应用程序的处理过程——当然,用户接口作为应用程序的一部分同样的好。
13.18.1 什么是Bean
在经细节处理后,一个组件在类中被独特的具体化,真正地成为一块代码。关键的争议在于应用程序构建工具发现组件的属性和事件能力。为了创建一个VB组件,程序开发者不得不编写正确的同时也是复杂烦琐的代码片,接下来由某些协议去展现它们的事件和属性。Delphi是第二代的可视化编程工具并且这种开发语言主动地围绕可视化编程来设计因此它更容易去创建一个可视化组件。但是,Java带来了可视化的创作组件做为Java Beans最高级的“装备”,因为一个Bean就是一个类。我们不必再为制造任何的Bean而编写一些特殊的代码或者使用特殊的编程语言。事实上,我们唯一需要做的是略微地修改我们对我们方法命名的办法。方法名通知应用程序构建工具是否是一个属性,一个事件或是一个普通的方法。
在Java的文件中,命名规则被错误地曲解为“设计模式”。这十分的不幸,因为设计模式(参见第16章)惹来不少的麻烦。命名规则不是设计模式,它是相当的简单:
(1) 因为属性被命名为xxx,我们代表性的创建两个方法:getXxx()和setXxx()。注意get或set后的第一个字母小写以产生属性名。get和set方法产生同样类型的参数。set和get的属性名和类型名之间没有关系。
(2) 对于布尔逻辑型属性,我们可以使用上面的get和set方法,但我们也可以用is代替 get。
(3) Bean的普通方法不适合上面的命名规则,但它们是公用的。
(4)对于事件,我们使用listener(接收器)方法。这种方法完全同我们看到过的方法相同:(addFooBarListener(FooBarListener)和removeFooBarListener(FooBarListener)方法用来处理FooBar事件。大多数时候内建的事件和接收器会满足我们的需要,但我们可以创建自己的事件和接收器接口。
上面的第一点回答了一个关于我们可能注意到的从Java 1.0到Java 1.1的改变的问题:一些方法的名字太过于短小,显然改写名字毫无意义。现在我们可以看到为了制造Bean中的特殊的组件,大多数的这些修改不得不适合于get和set命名规则。
现在,我们已经可以利用上面的这些指导方针去创建一个简单的Bean:
//: Frog.java// A trivial Java Beanpackage frogbean;import java.awt.*;import java.awt.event.*;class Spots {}public class Frog {private int jumps;private Color color;private Spots spots;private boolean jmpr;public int getJumps() { return jumps; }public void setJumps(int newJumps) {jumps = newJumps;}public Color getColor() { return color; }public void setColor(Color newColor) {color = newColor;}public Spots getSpots() { return spots; }public void setSpots(Spots newSpots) {spots = newSpots;}public boolean isJumper() { return jmpr; }public void setJumper(boolean j) { jmpr = j; }public void addActionListener(ActionListener l) {//...}public void removeActionListener(ActionListener l) {// ...}public void addKeyListener(KeyListener l) {// ...}public void removeKeyListener(KeyListener l) {// ...}// An "ordinary" public method:public void croak() {System.out.println("Ribbet!");}} ///:~
首先,我们可看到Bean就是一个类。通常,所有我们的字段会被作为专用,并且可以接近的唯一办法是通过方法。紧接着的是命名规则,属性是jump,color,jumper,spots(注意这些修改是在第一个字母在属性名的情况下进行的)。虽然内部确定的名字同最早的三个例子的属性名一样,在jumper中我们可以看到属性名不会强迫我们使用任何特殊的内部可变的名字(或者,真的拥有一些内部的可变的属性名)。
Bean事件的引用是ActionEvent和KeyEvent,这是根据有关接收器的add和remove命名方法得出的。最后我们可以注意到普通的方法croak()一直是Bean的一部分,仅仅是因为它是一个公共的方法,而不是因为它符合一些命名规则。
13.18.2 用Introspector提取BeanInfo
当我们拖放一个Bean的调色板并将它放入到窗体中时,一个Bean的最关键的部分的规则发生了。应用程序构建工具必须可以创建Bean(如果它是默认的构造器的话,它就可以做)然后,在此范围外访问Bean的源代码,提取所有的必要的信息以创立属性表和事件处理器。
解决方案的一部分在11章结尾部分已经显现出来:Java 1.1版的映象允许一个匿名类的所有方法被发现。这完美地解决了Bean的难题而无需我们使用一些特殊的语言关键字像在其它的可视化编程语言中所需要的那样。事实上,一个主要的原因是映象增加到Java 1.1版中以支持Beans(尽管映象同样支持对象串联和远程方法调用)。因为我们可能希望应用程序构建工具的开发者将不得不映象每个Bean并且通过它们的方法搜索以找到Bean的属性和事件。
这当然是可能的,但是Java的研制者们希望为每个使用它的用户提供一个标准的接口,而不仅仅是使Bean更为简单易用,不过他们也同样提供了一个创建更复杂的Bean的标准方法。这个接口就是Introspector类,在这个类中最重要的方法静态的getBeanInfo()。我们通过一个类处理这个方法并且getBeanInfo()方法全面地对类进行查询,返回一个我们可以进行详细研究以发现其属性、方法和事件的BeanInfo对象。
通常我们不会留意这样的一些事物——我们可能会使用我们大多数的现成的Bean,并且我们不需要了解所有的在底层运行的技术细节。我们会简单地拖放我们的Bean到我们窗体中,然后配置它们的属性并且为事件编写处理器。无论如何它都是一个有趣的并且是有教育意义的使用Introspector来显示关于Bean信息的练习,好啦,闲话少说,这里有一个工具请运行它(我们可以在forgbean子目录中找到它):
//: BeanDumper.java// A method to introspect a Beanimport java.beans.*;import java.lang.reflect.*;public class BeanDumper {public static void dump(Class bean){BeanInfo bi = null;try {bi = Introspector.getBeanInfo(bean, java.lang.Object.class);} catch(IntrospectionException ex) {System.out.println("Couldn't introspect " +bean.getName());System.exit(1);}PropertyDescriptor[] properties =bi.getPropertyDescriptors();for(int i = 0; i < properties.length; i++) {Class p = properties[i].getPropertyType();System.out.println("Property type:\n " + p.getName());System.out.println("Property name:\n " +properties[i].getName());Method readMethod =properties[i].getReadMethod();if(readMethod != null)System.out.println("Read method:\n " +readMethod.toString());Method writeMethod =properties[i].getWriteMethod();if(writeMethod != null)System.out.println("Write method:\n " +writeMethod.toString());System.out.println("====================");}System.out.println("Public methods:");MethodDescriptor[] methods =bi.getMethodDescriptors();for(int i = 0; i < methods.length; i++)System.out.println(methods[i].getMethod().toString());System.out.println("======================");System.out.println("Event support:");EventSetDescriptor[] events =bi.getEventSetDescriptors();for(int i = 0; i < events.length; i++) {System.out.println("Listener type:\n " +events[i].getListenerType().getName());Method[] lm =events[i].getListenerMethods();for(int j = 0; j < lm.length; j++)System.out.println("Listener method:\n " +lm[j].getName());MethodDescriptor[] lmd =events[i].getListenerMethodDescriptors();for(int j = 0; j < lmd.length; j++)System.out.println("Method descriptor:\n " +lmd[j].getMethod().toString());Method addListener =events[i].getAddListenerMethod();System.out.println("Add Listener Method:\n " +addListener.toString());Method removeListener =events[i].getRemoveListenerMethod();System.out.println("Remove Listener Method:\n " +removeListener.toString());System.out.println("====================");}}// Dump the class of your choice:public static void main(String[] args) {if(args.length < 1) {System.err.println("usage: \n" +"BeanDumper fully.qualified.class");System.exit(0);}Class c = null;try {c = Class.forName(args[0]);} catch(ClassNotFoundException ex) {System.err.println("Couldn't find " + args[0]);System.exit(0);}dump(c);}} ///:~
BeanDumper.dump()是一个可以做任何工作的方法。首先它试图创建一个BeanInfo对象,如果成功地调用BeanInfo的方法,就产生关于属性、方法和事件的信息。在Introspector.getBeanInfo()中,我们会注意到有一个另外的参数。由它来通知Introspector访问继承体系的地点。在这种情况下,它在分析所有对象方法前停下,因为我们对看到那些并不感兴趣。
因为属性,getPropertyDescriptors()返回一组的属性描述符号。对于每个描述符号我们可以调用getPropertyType()方法彻底的通过属性方法发现类的对象。这时,我们可以用getName()方法得到每个属性的假名(从方法名中提取),getname()方法用getReadMethod()和getWriteMethod()完成读和写的操作。最后的两个方法返回一个可以真正地用来调用在对象上调用相应的方法方法对象(这是映象的一部分)。对于公共方法(包括属性方法),getMethodDescriptors()返回一组方法描述字符。每一个我们都可以得到相当的方法对象并可以显示出它们的名字。
对于事件而言,getEventSetDescriptors()返回一组事件描述字符。它们中的每一个都可以被查询以找出接收器的类,接收器类的方法以及增加和删除接收器的方法。BeanDumper程序打印出所有的这些信息。
如果我们调用BeanDumper在Frog类中,就像这样:
java BeanDumper frogbean.Frog
它的输出结果如下(已删除这儿不需要的额外细节):
class name: FrogProperty type:ColorProperty name:colorRead method:public Color getColor()Write method:public void setColor(Color)====================Property type:SpotsProperty name:spotsRead method:public Spots getSpots()Write method:public void setSpots(Spots)====================Property type:booleanProperty name:jumperRead method:public boolean isJumper()Write method:public void setJumper(boolean)====================Property type:intProperty name:jumpsRead method:public int getJumps()Write method:public void setJumps(int)====================Public methods:public void setJumps(int)public void croak()public void removeActionListener(ActionListener)public void addActionListener(ActionListener)public int getJumps()public void setColor(Color)public void setSpots(Spots)public void setJumper(boolean)public boolean isJumper()public void addKeyListener(KeyListener)public Color getColor()public void removeKeyListener(KeyListener)public Spots getSpots()======================Event support:Listener type:KeyListenerListener method:keyTypedListener method:keyPressedListener method:keyReleasedMethod descriptor:public void keyTyped(KeyEvent)Method descriptor:public void keyPressed(KeyEvent)Method descriptor:public void keyReleased(KeyEvent)Add Listener Method:public void addKeyListener(KeyListener)Remove Listener Method:public void removeKeyListener(KeyListener)====================Listener type:ActionListenerListener method:actionPerformedMethod descriptor:public void actionPerformed(ActionEvent)Add Listener Method:public void addActionListener(ActionListener)Remove Listener Method:public void removeActionListener(ActionListener)====================
这个结果揭示出了Introspector在从我们的Bean产生一个BeanInfo对象时看到的大部分内容。我们可注意到属性的类型和它们的名字是相互独立的。请注意小写的属性名。(当属性名开头在一行中有超过不止的大写字母,这一次程序就不会被执行。)并且请记住我们在这里所见到的方法名(例如读和与方法)真正地从一个可以被用来在对象中调用相关方法的方法对象中产生。
通用方法列表包含了不相关的事件或者属性,例如croak()。列表中所有的方法都是我们可以有计划的为Bean调用,并且应用程序构建工具可以选择列出所有的方法,当我们调用方法时,减轻我们的任务。
最后,我们可以看到事件在接收器中完全地分析研究它的方法、增加和减少接收器的方法。基本上,一旦我们拥有BeanInfo,我们就可以找出对Bean来说任何重要的事物。我们同样可以为Bean调用方法,即使我们除了对象外没有任何其它的信息(此外,这也是映象的特点)。
13.18.3 一个更复杂的Bean
接下的程序例子稍微复杂一些,尽管这没有什么价值。这个程序是一张不论鼠标何时移动都围绕它画一个小圆的,并且一个动作接收器被激活。画布。当按下鼠标键时,我们可以改变的属性是圆的大小,除此之外还有被显示文字的色彩,大小,内容。BangBean同样拥有它自己的addActionListener()和removeActionListener()方法,因此我们可以附上自己的当用户单击在BangBean上时会被激活的接收器。这样,我们将能够确认可支持的属性和事件:
//: BangBean.java// A graphical Beanpackage bangbean;import java.awt.*;import java.awt.event.*;import java.io.*;import java.util.*;public class BangBean extends Canvasimplements Serializable {protected int xm, ym;protected int cSize = 20; // Circle sizeprotected String text = "Bang!";protected int fontSize = 48;protected Color tColor = Color.red;protected ActionListener actionListener;public BangBean() {addMouseListener(new ML());addMouseMotionListener(new MML());}public int getCircleSize() { return cSize; }public void setCircleSize(int newSize) {cSize = newSize;}public String getBangText() { return text; }public void setBangText(String newText) {text = newText;}public int getFontSize() { return fontSize; }public void setFontSize(int newSize) {fontSize = newSize;}public Color getTextColor() { return tColor; }public void setTextColor(Color newColor) {tColor = newColor;}public void paint(Graphics g) {g.setColor(Color.black);g.drawOval(xm - cSize/2, ym - cSize/2,cSize, cSize);}// This is a unicast listener, which is// the simplest form of listener management:public void addActionListener (ActionListener l)throws TooManyListenersException {if(actionListener != null)throw new TooManyListenersException();actionListener = l;}public void removeActionListener(ActionListener l) {actionListener = null;}class ML extends MouseAdapter {public void mousePressed(MouseEvent e) {Graphics g = getGraphics();g.setColor(tColor);g.setFont(new Font("TimesRoman", Font.BOLD, fontSize));int width =g.getFontMetrics().stringWidth(text);g.drawString(text,(getSize().width - width) /2,getSize().height/2);g.dispose();// Call the listener's method:if(actionListener != null)actionListener.actionPerformed(new ActionEvent(BangBean.this,ActionEvent.ACTION_PERFORMED, null));}}class MML extends MouseMotionAdapter {public void mouseMoved(MouseEvent e) {xm = e.getX();ym = e.getY();repaint();}}public Dimension getPreferredSize() {return new Dimension(200, 200);}// Testing the BangBean:public static void main(String[] args) {BangBean bb = new BangBean();try {bb.addActionListener(new BBL());} catch(TooManyListenersException e) {}Frame aFrame = new Frame("BangBean Test");aFrame.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);}});aFrame.add(bb, BorderLayout.CENTER);aFrame.setSize(300,300);aFrame.setVisible(true);}// During testing, send action information// to the console:static class BBL implements ActionListener {public void actionPerformed(ActionEvent e) {System.out.println("BangBean action");}}} ///:~
最重要的是我们会注意到BangBean执行了这种串联化的接口。这意味着应用程序构建工具可以在程序设计者调整完属性值后利用串联为BangBean贮藏所有的信息。当Bean作为运行的应用程序的一部分被创建时,那些被贮藏的属性被重新恢复,因此我们可以正确地得到我们的设计。
我们能看到通常同Bean一起运行的所有的字段都是专用的——允许只能通过方法来访问,通常利用“属性”结构。
当我们注视着addActionListener()的签名时,我们会注意到它可以产生出一个TooManyListenerException(太多接收器异常)。这个异常指明它是一个单一的类型的,意味着当事件发生时,它只能通知一个接收器。一般情况下,我们会使用具有多种类型的事件,以便一个事件通知多个的接收器。但是,那样会陷入直到下一章我们才能准备好的结局中,因此这些内容会被重新回顾(下一个标题是“Java Beans 的重新回顾”)。单一类型的事件回避了这个难题。
当我们按下鼠标键时,文字被安入BangBean中间,并且如果动作接收器字段存在,它的actionPerformed()方法就被调用,创建一个新的ActionEvent对象在处理过程中。无论何时鼠标移动,它的新座标将被捕捉,并且画布会被重画(像我们所看到的抹去一些画布上的文字)。
main()方法增加了允许我们从命令行中测试程序的功能。当一个Bean在一个开发环境中,main()方法不会被使用,但拥有它是绝对有益的,因为它提供了快捷的测试能力。无论何时一个ActionEvent发生,main()方法都将创建了一个帧并安置了一个BangBean在它里面,还在BangBean中附上了一个简单的动作接收器以打印到控制台。当然,一般来说应用程序构建工具将创建大多数的Bean的代码。当我们通过BeanDumper或者安放BangBean到一个可激活Bean的开发环境中去运行BangBean时,我们会注意到会有很多额外的属性和动作明显超过了上面的代码。那是因为BangBean从画布中继承,并且画布就是一个Bean,因此我们看到它的属性和事件同样的合适。
13.18.4 Bean的封装
在我们可以安放一个Bean到一个可激活Bean的可视化构建工具中前,它必须被放入到标准的Bean容器里,也就是包含Bean类和一个表示“这是一个Bean”的清单文件的JAR(Java ARchive,Java文件)文件中。清单文件是一个简单的紧随事件结构的文本文件。对于BangBean而言,清单文件就像下面这样:
Manifest-Version: 1.0Name: bangbean/BangBean.classJava-Bean: True
其中,第一行指出清单文件结构的版本,这是SUN公司在很久以前公布的版本。第二行(空行忽略)对文件命名为BangBean.class。第三行表示“这个文件是一个Bean”。没有第三行,程序构建工具不会将类作为一个Bean来认可。
唯一难以处理的部分是我们必须肯定Name:字段中的路径是正确的。如果我们回顾BangBean.java,我们会看到它在package bangbean(因为存放类路径的子目录称为bangbean)中,并且这个名字在清单文件中必须包括封装的信息。另外,我们必须安放清单文件在我们封装路径的根目录上,在这个例子中意味着安放文件在bangbean子目录中。这之后,我们必须从同一目录中调用Jar来作为清单文件,如下所示:
jar cfm BangBean.jar BangBean.mf bangbean
这个例子假定我们想产生一个名为BangBean.jar的文件并且我们将清单放到一个称为BangBean.mf文件中。
我们可能会想“当我编译BangBean.java时,产生的其它类会怎么样呢?”哦,它们会在bangbean子目录中被中止,并且我们会注意到上面jar命令行的最后一个参数就是bangbean子目录。当我们给jar子目录名时,它封装整个的子目录到jar文件中(在这个例子中,包括BangBean.java的源代码文件——对于我们自己的Bean我们可能不会去选择包含源代码文件。)另外,如果我们改变主意,解开打包的JAR文件,我们会发现我们清单文件并不在里面,但jar创建了它自己的清单文件(部分根据我们的文件),称为MAINFEST.MF并且安放它到META-INF子目录中(代表“meta-information”)。如果我们打开这个清单文件,我们同样会注意到jar为每个文件加入数字签名信息,其结构如下:
Digest-Algorithms: SHA MD5SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0=MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==
一般来说,我们不必担心这些,如果我们要做一些修改,可以修改我们的原始的清单文件并且重新调用jar以为我们的Bean创建了一个新的JAR文件。我们同样也可以简单地通过增加其它的Bean的信息到我们清单文件来增加它们到JAR文件中。
值得注意的是我们或许需要安放每个Bean到它自己的子目录中,因为当我们创建一个JAR文件时,分配JAR应用目录名并且JAR放置子目录中的任何文件到JAR文件中。我们可以看到Frog和BangBean都在它们自己的子目录中。
一旦我们将我们的Bean正确地放入一个JAR文件中,我们就可以携带它到一个可以激活Bean的编程环境中使用。使用这种方法,我们可以从一种工具到另一种工具间交替变换,但SUN公司为Java Beans提供了免费高效的测试工具在它们的“Bean Development Kit,Bean开发工具”(BDK)称为beanbox。(我们可以从www.javasoft.com处下载。)在我们启动beanbox前,放置我们的Bean到beanbox中,复制JAR文件到BDK的jars子目录中。
13.18.5 更复杂的Bean支持
我们可以看到创建一个Bean显然多么的简单。在程序设计中我们几乎不受到任何的限制。Java Bean的设计提供了一个简单的输入点,这样可以提高到更复杂的层次上。这些高层次的问题超出了这本书所要讨论的范围,但它们会在此做简要的介绍。我们可以在http://java.sun.com/beans上找到更多的详细资料。
我们增加更加复杂的程序和它的属性到一个位置。上面的例子显示一个独特的属性,当然它也可能代表一个数组的属性。这称为索引属性。我们简单地提供一个相应的方法(再者有一个方法名的命名规则)并且Introspector认可索引属性,因此我们的应用程序构建工具相应的处理。
属性可以被捆绑,这意味着它们将通过PropertyChangeEvent通知其它的对象。其它的对象可以随后根据对Bean的改变选择修改它们自己。
属性可以被束缚,这意味着其它的对象可以在一个属性的改变不能被接受时,拒绝它。其它的对象利用一个PropertyChangeEvent来通知,并且它们产生一个ProptertyVetoException去阻止修改的发生,并恢复为原来的值。
我们同样能够改变我们的Bean在设计时的被描绘成的方法:
(1) 我们可以为我们特殊的Bean提供一个定制的属性表。这个普通的属性表将被所有的Bean所使用,但当我们的Bean被选择时,它会自动地调用这张属性表。
(2) 我们可以为一个特殊的属性创建一个定制的编辑器,因此普通的属性表被使用,但当我们指定的属性被调用时,编辑器会自动地被调用。
(3)我们可以为我们的Bean提供一个定制的BeanInfo类,产生的信息不同于由Introspector默认产生的。
(4) 它同样可能在所有的FeatureDescriptors中改变expert的开关模式,以辨别基本特征和更复杂的特征。
13.18.6 Bean更多的知识
另外有关的争议是Bean不能被编址。无论何时我们创建一个Bean,都希望它会在一个多线程的环境中运行。这意味着我们必须理解线程的出口,我们将在下一章中介绍。我们会发现有一段称为“Java Beans的回顾”的节会注意到这个问题和它的解决方案。
